package com.lijiajia.cloud.common.security.access.prepost;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.prepost.*;
import org.springframework.util.ClassUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
 * <tt>MethodSecurityMetadataSource</tt> 用于从放置在方法上的 @PreFilter 和 @PreAuthorize
 * 注解中提取元数据. 该类仅负责查找相关注释)如果有). 它将实际的 <tt>ConfigAttribute</tt>
 * 创建委托给其 {@link PrePostInvocationAttributeFactory}, 从而将其自身与将强制执行注解行为的机制分离
 * <p>
 * 可以在类或方法上指定注解,并且特定于方法的注释优先.
 * 如果您使用任何注释并且未指定预授权条件, 则将允许该方法，就像存在 @PreAuthorize("permitAll") 一样
 * <p>
 * 由于我们在这里处理多个注释, 因此我们可能必须将在多个位置定义的注释组合为单个方法 - 它们可以在方法本身上定义, 也可以在接口或类级别定义.
 *
 * @author lijiajia
 * @see PreInvocationAuthorizationAdviceVoter
 * @since 2019-05-31
 */
public class CustomPrePostAnnotationSecurityMetadataSource extends PrePostAnnotationSecurityMetadataSource {
    private final String ACCESS_OR_SUPER_ADMIN = " or isSuperAdmin()";
    private final PrePostInvocationAttributeFactory attributeFactory;

    public CustomPrePostAnnotationSecurityMetadataSource(PrePostInvocationAttributeFactory attributeFactory) {
        super(attributeFactory);
        this.attributeFactory = attributeFactory;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return Collections.emptyList();
        }

        logger.trace("Looking for Pre/Post annotations for method '" + method.getName()
                + "' on target class '" + targetClass + "'");
        PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);
        PreAuthorize preAuthorize = findAnnotation(method, targetClass,
                PreAuthorize.class);
        PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);
        // TODO: Can we check for void methods and throw an exception here?
        PostAuthorize postAuthorize = findAnnotation(method, targetClass,
                PostAuthorize.class);

        if (preFilter == null && preAuthorize == null && postFilter == null
                && postAuthorize == null) {
            // There is no meta-data so return
            logger.trace("No expression annotations found");
            return Collections.emptyList();
        }

        String preFilterAttribute = preFilter == null ? null : preFilter.value();
        String filterObject = preFilter == null ? null : preFilter.filterTarget();
        String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value().concat(ACCESS_OR_SUPER_ADMIN);
        String postFilterAttribute = postFilter == null ? null : postFilter.value();
        String postAuthorizeAttribute = postAuthorize == null ? null : postAuthorize.value().concat(ACCESS_OR_SUPER_ADMIN);

        ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);

        PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(
                preFilterAttribute, filterObject, preAuthorizeAttribute);

        if (pre != null) {
            attrs.add(pre);
        }

        PostInvocationAttribute post = attributeFactory.createPostInvocationAttribute(
                postFilterAttribute, postAuthorizeAttribute);

        if (post != null) {
            attrs.add(post);
        }

        attrs.trimToSize();

        return attrs;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return super.getAllConfigAttributes();
    }

    /**
     * See
     * {@link org.springframework.security.access.method.AbstractFallbackMethodSecurityMetadataSource#getAttributes(Method, Class)}
     * 对于这种方法的逻辑。这里的排序与我们略有不同
     * 在类级别之前考虑接口上的特定于方法的注释。
     */
    private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass,
                                                    Class<A> annotationClass) {
        // The method may be on an interface, but we need attributes from the target
        // class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        A annotation = AnnotationUtils.findAnnotation(specificMethod, annotationClass);

        if (annotation != null) {
            logger.debug(annotation + " found on specific method: " + specificMethod);
            return annotation;
        }

        // Check the original (e.g. interface) method
        if (specificMethod != method) {
            annotation = AnnotationUtils.findAnnotation(method, annotationClass);

            if (annotation != null) {
                logger.debug(annotation + " found on: " + method);
                return annotation;
            }
        }

        // Check the class-level (note declaringClass, not targetClass, which may not
        // actually implement the method)
        annotation = AnnotationUtils.findAnnotation(specificMethod.getDeclaringClass(),
                annotationClass);

        if (annotation != null) {
            logger.debug(annotation + " found on: "
                    + specificMethod.getDeclaringClass().getName());
            return annotation;
        }

        return null;
    }
}
